package shutil

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"reflect"
	"time"
)

// Raised when trying to do a kind of operation (e.g. copying) which is
// not supported on a special file (e.g. a named pipe)
type SpecialFileError struct {
	File     string
	FileInfo os.FileInfo
}

func (this SpecialFileError) Error() string {
	return fmt.Sprintf("%s is a named pipe", this.File)
}

// Copy data and mode bits ("cp src dst").
// The destination may be a directory.
func Copy(src, dst string) error {
	dFileInfo, err := os.Stat(dst)
	if err == nil {
		if dFileInfo.IsDir() {
			dst = path.Join(dst, path.Base(src))
		}
	}
	err = copyFile(src, dst)
	if err != nil {
		return err
	}
	return copyMode(src, dst)
}

// Copy data and all stat info ("cp -p src dst").
// The destination may be a directory.
func Copy2(src, dst string) error {
	dFileInfo, err := os.Stat(dst)
	if err == nil {
		if dFileInfo.IsDir() {
			dst = path.Join(dst, path.Base(src))
		}
	}
	err = copyFile(src, dst)
	if err != nil {
		return err
	}
	return copyStat(src, dst)
}

func copyFileObject(fsrc io.Reader, fdst io.Writer) (int64, error) {
	return io.Copy(fdst, fsrc)
}

func sameFile(fsrc, fdst string) (bool, error) {
	fsrcInfo, err := os.Stat(fsrc)
	if err != nil {
		return false, err
	}
	fdstInfo, err := os.Stat(fdst)
	if err != nil {
		return false, nil
	}
	return os.SameFile(fsrcInfo, fdstInfo), nil
}

func copyFile(src, dst string) error {
	same, err := sameFile(src, dst)
	if err != nil {
		return err
	}
	if same {
		return errors.New(fmt.Sprintf("%s and %s are the same file.", src, dst))
	}

	specialFile := func(fi os.FileInfo) bool {
		return (fi.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe
	}

	srcFileInfo, err := os.Lstat(src)
	if err != nil {
		return err
	}
	if specialFile(srcFileInfo) {
		return &SpecialFileError{src, srcFileInfo}
	}
	dstFileInfo, err := os.Stat(dst)
	if err == nil {
		if specialFile(dstFileInfo) {
			return &SpecialFileError{dst, dstFileInfo}
		}
	}
	srcfile, err := os.Open(src)
	if err != nil {
		return err
	}
	dstfile, err := os.Create(dst)
	if err != nil {
		return err
	}
	_, err = copyFileObject(srcfile, dstfile)
	return err
}

func copyMode(fsrc, fdst string) error {
	sFileInfo, err := os.Stat(fsrc)
	if err != nil {
		return err
	}
	return os.Chmod(fdst, sFileInfo.Mode())
}

func copyStat(fsrc, fdst string) error {
	sFileInfo, err := os.Stat(fsrc)
	if err != nil {
		return err
	}
	// Atime and Mtime
	atime := reflect.ValueOf(sFileInfo.Sys()).Elem().FieldByName("Atim")
	mtime := reflect.ValueOf(sFileInfo.Sys()).Elem().FieldByName("Mtim")
	err = os.Chtimes(fdst, time.Unix(atime.Field(0).Int(), atime.Field(1).Int()),
		time.Unix(mtime.Field(0).Int(), mtime.Field(1).Int()))
	if err != nil {
		return err
	}
	return os.Chmod(fdst, sFileInfo.Mode())
}
